Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions src/packages/frontend/frame-editors/code-editor/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ export class Actions<
delete this._cm[id];
}

if (type != "terminal") {
if (type != "terminal" && type != "shell") {
this.terminals.close_terminal(id);
}

Expand Down Expand Up @@ -1500,7 +1500,8 @@ export class Actions<
_get_most_recent_shell_id(command?: string): string | undefined {
return this._get_most_recent_active_frame_id(
(node) =>
node.get("type").slice(0, 8) == "terminal" &&
(node.get("type").slice(0, 8) == "terminal" ||
node.get("type") == "shell") &&
node.get("command") == command,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/packages/frontend/frame-editors/frame-tree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type EditorType =
| "sagews-document"
| "search"
| "settings"
| "shell"
| "slate"
| "slides-notes"
| "slides-slideshow"
Expand Down
60 changes: 58 additions & 2 deletions src/packages/frontend/frame-editors/jupyter-editor/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* This file is part of CoCalc: Copyright © 2020-2025 Sagemath, Inc.
* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

Expand All @@ -18,7 +18,7 @@ import {
Actions as BaseActions,
CodeEditorState,
} from "../code-editor/actions";
import { FrameTree } from "../frame-tree/types";
import type { FrameDirection, FrameTree } from "../frame-tree/types";
import { NotebookFrameActions } from "./cell-notebook/actions";
import {
close_jupyter_actions,
Expand Down Expand Up @@ -301,6 +301,62 @@ export class JupyterEditorActions extends BaseActions<JupyterEditorState> {
};
}

// Override to create "shell" type frames (shown as "Console" in the title
// bar) instead of generic "terminal" frames.
public async shell(id: string, no_switch: boolean = false): Promise<void> {
const spec = await this.get_shell_spec(id);
if (spec == null) {
// No kernel connection yet — fall back to generic terminal
return super.shell(id, no_switch);
}
const { command, args } = spec;
// Reuse an existing console frame if one exists
let shell_id: string | undefined = this._get_most_recent_shell_id(command);
if (shell_id == null) {
shell_id = this.split_frame("col", id, "shell", { command, args });
if (!shell_id) return;
} else {
this.terminals.set_command(shell_id, command, args);
this.set_frame_tree({ id: shell_id, command, args });
}
if (no_switch) return;
this.unset_frame_full();
await delay(1);
if (this.isClosed()) return;
this.set_active_id(shell_id);
}

public new_frame(
type: string,
direction?: FrameDirection,
first?: boolean,
): string {
if (type === "shell") {
const id = super.new_frame(type, direction, first);
this.setShellFrameCommand(id);
return id;
}
return super.new_frame(type, direction, first);
}

set_frame_type(id: string, type: string): void {
super.set_frame_type(id, type);
if (type === "shell") {
this.setShellFrameCommand(id);
}
}

private setShellFrameCommand(id: string): void {
const connection_file = this.jupyter_actions?.store?.get("connection_file");
if (connection_file) {
this.set_frame_tree({
id,
command: "jupyter",
args: ["console", "--existing", connection_file],
});
}
}

// Not an action, but works to make code clean
has_format_support(id: string, available_features?): false | string {
id = id;
Expand Down
13 changes: 12 additions & 1 deletion src/packages/frontend/frame-editors/jupyter-editor/editor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

Expand All @@ -24,6 +24,7 @@ import { capitalize, field_cmp, set } from "@cocalc/util/misc";
import { createEditor } from "../frame-tree/editor";
import { EditorDescription } from "../frame-tree/types";
import { terminal } from "../terminal-editor/editor";
import { TerminalFrame } from "../terminal-editor/terminal";
import { time_travel } from "../time-travel-editor/editor";
import { CellNotebook } from "./cell-notebook/cell-notebook";
import { Introspect } from "./introspect/introspect";
Expand Down Expand Up @@ -130,6 +131,15 @@ const introspect: EditorDescription = {
commands: set(["decrease_font_size", "increase_font_size"]),
} as const;

const jupyter_console: EditorDescription = {
type: "shell",
short: jupyter.editor.console_label,
name: jupyter.editor.console_label,
icon: "ipynb",
component: TerminalFrame,
commands: terminal.commands,
} as const;

const jupyter_json: EditorDescription = {
type: "jupyter_json_view",
short: jupyter.editor.raw_json_view_short,
Expand All @@ -154,6 +164,7 @@ export const EDITOR_SPEC = {
jupyter_slideshow_revealjs,
jupyter_table_of_contents,
introspect,
shell: jupyter_console,
terminal,
time_travel,
jupyter_json,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* This file is part of CoCalc: Copyright © 2026 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

/**
* Convert args from an Immutable.js List (or other iterable) to a plain string[].
*
* When frame tree node data is read via node.get("args"), the result is an
* Immutable.js List rather than a plain array. Downstream code (e.g. MsgPack
* serialization over Conat) requires plain arrays, so we normalise here.
*/
export function normalizeArgs(rawArgs: unknown): string[];
export function normalizeArgs(
rawArgs: unknown,
allowUndefined: true,
): string[] | undefined;
export function normalizeArgs(
rawArgs: unknown,
allowUndefined?: boolean,
): string[] | undefined {
if (rawArgs == null) {
return allowUndefined ? undefined : [];
}
const iter = Array.isArray(rawArgs)
? rawArgs
: typeof (rawArgs as any)?.toArray === "function"
? (rawArgs as any).toArray()
: typeof (rawArgs as any)?.[Symbol.iterator] === "function"
? Array.from(rawArgs as Iterable<unknown>)
: undefined;
if (iter == null) {
return allowUndefined ? undefined : [];
}
return iter.filter((arg): arg is string => typeof arg === "string");
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

Expand All @@ -11,6 +11,7 @@ import { Actions, CodeEditorState } from "../code-editor/actions";
import * as tree_ops from "../frame-tree/tree-ops";
import { close, len } from "@cocalc/util/misc";
import { Terminal } from "./connected-terminal";
import { normalizeArgs } from "./normalize-args";

export class TerminalManager<T extends CodeEditorState = CodeEditorState> {
private terminals: { [key: string]: Terminal<T> } = {};
Expand Down Expand Up @@ -39,7 +40,8 @@ export class TerminalManager<T extends CodeEditorState = CodeEditorState> {
const numbers = {};
for (let id0 in this.actions._get_leaf_ids()) {
const node0 = tree_ops.get_node(this.actions._get_tree(), id0);
if (node0 == null || node0.get("type") != "terminal") {
const nodeType = node0?.get("type");
if (node0 == null || (nodeType != "terminal" && nodeType != "shell")) {
continue;
}
let n = node0.get("number");
Expand Down Expand Up @@ -74,7 +76,7 @@ export class TerminalManager<T extends CodeEditorState = CodeEditorState> {
let args: string[] | undefined = undefined;
if (node != null) {
command = node.get("command");
args = node.get("args");
args = normalizeArgs(node.get("args"), true);
}
this.terminals[id] = new Terminal<T>(
this.actions,
Expand Down
16 changes: 5 additions & 11 deletions src/packages/frontend/frame-editors/terminal-editor/terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

Expand All @@ -22,6 +22,7 @@ import { ComputeServerDocStatus } from "@cocalc/frontend/compute/doc-status";
import useResizeObserver from "use-resize-observer";
import useComputeServerId from "@cocalc/frontend/compute/file-hook";
import { termPath } from "@cocalc/util/terminal/names";
import { normalizeArgs } from "./normalize-args";

interface Props {
actions: any;
Expand Down Expand Up @@ -158,16 +159,9 @@ export const TerminalFrame: React.FC<Props> = React.memo((props: Props) => {
function render_command(): Rendered {
const command = props.desc.get("command");
if (!command) return;
const args: string[] = props.desc.get("args") ?? [];
// Quote if args have spaces:
for (let i = 0; i < args.length; i++) {
if (/\s/.test(args[i])) {
// has whitespace -- this is not bulletproof, since
// args[i] could have a " in it. But this is just for
// display purposes, so it doesn't have to be bulletproof.
args[i] = `"${args[i]}"`;
}
}
const args = normalizeArgs(props.desc.get("args")).map((arg) =>
/\s/.test(arg) ? `"${arg}"` : arg,
);
return (
<div style={COMMAND_STYLE}>
{command} {args.join(" ")}
Expand Down
16 changes: 15 additions & 1 deletion src/packages/project/conat/terminal/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class Session {
TMUX: undefined, // ensure not set
};
const command = this.options.command ?? DEFAULT_COMMAND;
const args = this.options.args ?? [];
const args = normalizeArgs(this.options.args);
const initFilename: string = console_init_filename(this.termPath);
if (await exists(initFilename)) {
args.push("--init-file");
Expand Down Expand Up @@ -421,6 +421,20 @@ function getCWD(pathHead, cwd?): string {
return pathHead;
}

function normalizeArgs(rawArgs: unknown): string[] {
const iter = Array.isArray(rawArgs)
? rawArgs
: typeof (rawArgs as any)?.toArray === "function"
? (rawArgs as any).toArray()
: typeof (rawArgs as any)?.[Symbol.iterator] === "function"
? Array.from(rawArgs as Iterable<unknown>)
: undefined;
if (iter == null) {
return [];
}
return iter.filter((arg): arg is string => typeof arg === "string");
}

function historyFile(path: string): string | undefined {
if (path.startsWith("/")) {
// only set histFile for paths in the home directory i.e.,
Expand Down
Loading