Skip to content
Open
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
12 changes: 12 additions & 0 deletions Code.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,8 @@
947BBCF82C12F5AA00FFD0C5 /* TreeSitterYAMLRunestone in Frameworks */ = {isa = PBXBuildFile; productRef = 947BBCF72C12F5AA00FFD0C5 /* TreeSitterYAMLRunestone */; };
947BF349262453040015DAEB /* SearchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947BF348262453040015DAEB /* SearchManager.swift */; };
94801F93266FB5E400B29D80 /* TerminalInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94801F92266FB5E400B29D80 /* TerminalInstance.swift */; };
94801F97266FB5E500B29D80 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94801F96266FB5E500B29D80 /* TerminalManager.swift */; };
94801F98266FB5E500B29D80 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94801F96266FB5E500B29D80 /* TerminalManager.swift */; };
94801F95266FBC3500B29D80 /* ViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94801F94266FBC3500B29D80 /* ViewRepresentable.swift */; };
948488F92BE61684004D4A70 /* TerminalKeyboardToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948488F82BE61684004D4A70 /* TerminalKeyboardToolbar.swift */; };
948488FA2BE61684004D4A70 /* TerminalKeyboardToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948488F82BE61684004D4A70 /* TerminalKeyboardToolbar.swift */; };
Expand Down Expand Up @@ -723,6 +725,8 @@
94A77829257BC951008FE7B2 /* DocumentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77828257BC951008FE7B2 /* DocumentPickerView.swift */; };
94A7782D257BCEE2008FE7B2 /* getRootDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A7782C257BCEE2008FE7B2 /* getRootDirectory.swift */; };
94A77830257BD680008FE7B2 /* EditorTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A7782F257BD680008FE7B2 /* EditorTabs.swift */; };
94A77831257BD681008FE7B2 /* TerminalTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77830257BD681008FE7B2 /* TerminalTabBar.swift */; };
94A77832257BD682008FE7B2 /* TerminalTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77830257BD681008FE7B2 /* TerminalTabBar.swift */; };
94A77832257BDC50008FE7B2 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77831257BDC50008FE7B2 /* SafariView.swift */; };
94A77834257BE2D8008FE7B2 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77833257BE2D8008FE7B2 /* EditorView.swift */; };
94A7FFB3268D085300369147 /* BottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A7FFB2268D085300369147 /* BottomBar.swift */; };
Expand Down Expand Up @@ -1833,6 +1837,7 @@
947A9D8827D3C562007680C3 /* UIApplication+getSafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+getSafeArea.swift"; sourceTree = "<group>"; };
947BF348262453040015DAEB /* SearchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchManager.swift; sourceTree = "<group>"; };
94801F92266FB5E400B29D80 /* TerminalInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalInstance.swift; sourceTree = "<group>"; };
94801F96266FB5E500B29D80 /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
94801F94266FBC3500B29D80 /* ViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewRepresentable.swift; sourceTree = "<group>"; };
948488F82BE61684004D4A70 /* TerminalKeyboardToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalKeyboardToolbar.swift; sourceTree = "<group>"; };
9484BDD02B46FAEE003BCB8A /* injection.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = injection.js; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1902,6 +1907,7 @@
94A77828257BC951008FE7B2 /* DocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerView.swift; sourceTree = "<group>"; };
94A7782C257BCEE2008FE7B2 /* getRootDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = getRootDirectory.swift; sourceTree = "<group>"; };
94A7782F257BD680008FE7B2 /* EditorTabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabs.swift; sourceTree = "<group>"; };
94A77830257BD681008FE7B2 /* TerminalTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTabBar.swift; sourceTree = "<group>"; };
94A77831257BDC50008FE7B2 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
94A77833257BE2D8008FE7B2 /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = "<group>"; };
94A7FFB2268D085300369147 /* BottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomBar.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2958,6 +2964,7 @@
94A045FD2804853200182275 /* DescriptionText.swift */,
948E478126502B6900A6110D /* EditorTab.swift */,
94A7782F257BD680008FE7B2 /* EditorTabs.swift */,
94A77830257BD681008FE7B2 /* TerminalTabBar.swift */,
94F6B50F280EFD07000DBAE2 /* FileDisplayName.swift */,
94A77791257AB805008FE7B2 /* FileIcon.swift */,
94A046002804865000182275 /* SideBarButton.swift */,
Expand Down Expand Up @@ -3140,6 +3147,7 @@
94A777BD257B74EE008FE7B2 /* NotificationManager.swift */,
942ACB9C281312900067114D /* SpellChecker.swift */,
94801F92266FB5E400B29D80 /* TerminalInstance.swift */,
94801F96266FB5E500B29D80 /* TerminalManager.swift */,
9F046C312922203E00BDE4E9 /* ToolbarManager.swift */,
9F046C3429222D8E00BDE4E9 /* ExtensionManager.swift */,
9FA1225B2A8B209500E7B417 /* CodeAppContributionPointManager.swift */,
Expand Down Expand Up @@ -3696,6 +3704,7 @@
942ACB9E281312900067114D /* SpellChecker.swift in Sources */,
94E6CC7C2806FBAF00939E4F /* SFTPFileSystemProvider.swift in Sources */,
94196973280316C7008AAEB2 /* EditorTabs.swift in Sources */,
94A77832257BD682008FE7B2 /* TerminalTabBar.swift in Sources */,
94196974280316C7008AAEB2 /* LocalGitServiceProvider.swift in Sources */,
9474D2982B6B4B1300CCC530 /* EditorImplemenationView.swift in Sources */,
94196975280316C7008AAEB2 /* SourceControlIdentityConfiguration.swift in Sources */,
Expand Down Expand Up @@ -3739,6 +3748,7 @@
947313132BDFB80D004A9960 /* ExtensionCommunicationHelper.swift in Sources */,
94196986280316C7008AAEB2 /* node.swift in Sources */,
94196988280316C7008AAEB2 /* TerminalInstance.swift in Sources */,
94801F98266FB5E500B29D80 /* TerminalManager.swift in Sources */,
94196989280316C7008AAEB2 /* convertCArguments.swift in Sources */,
94FF337528435158003DE5DD /* SettingsFontPicker.swift in Sources */,
9F062FEA2B58D416006210AA /* TableView.swift in Sources */,
Expand Down Expand Up @@ -3888,6 +3898,7 @@
942ACB9D281312900067114D /* SpellChecker.swift in Sources */,
94E6CC7B2806FBAF00939E4F /* SFTPFileSystemProvider.swift in Sources */,
94A77830257BD680008FE7B2 /* EditorTabs.swift in Sources */,
94A77831257BD681008FE7B2 /* TerminalTabBar.swift in Sources */,
944FC3FF25C543CD00C7C43C /* LocalGitServiceProvider.swift in Sources */,
9474D2972B6B4B1300CCC530 /* EditorImplemenationView.swift in Sources */,
9494A3A52587479F004A103E /* SourceControlIdentityConfiguration.swift in Sources */,
Expand Down Expand Up @@ -3931,6 +3942,7 @@
947313122BDFB80D004A9960 /* ExtensionCommunicationHelper.swift in Sources */,
949B3CC725DEAAA700BC83B5 /* node.swift in Sources */,
94801F93266FB5E400B29D80 /* TerminalInstance.swift in Sources */,
94801F97266FB5E500B29D80 /* TerminalManager.swift in Sources */,
94F2FEA926A2EB0E007EBC6D /* convertCArguments.swift in Sources */,
94FF337428435158003DE5DD /* SettingsFontPicker.swift in Sources */,
9F062FE92B58D416006210AA /* TableView.swift in Sources */,
Expand Down
6 changes: 3 additions & 3 deletions CodeApp/Containers/MainScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ struct MainScene: View {
}
App.monacoInstance.theme = EditorTheme(
dark: ThemeManager.darkTheme, light: ThemeManager.lightTheme)
App.terminalInstance.applyTheme(rawTheme: theme.dictionary)
App.terminalManager.applyThemeToAll(rawTheme: theme.dictionary)
}
)
}
Expand Down Expand Up @@ -154,7 +154,7 @@ private struct MainView: View {
panelHeight = 200
}
isPanelVisible.toggle()
App.terminalInstance.webView.becomeFirstResponder()
App.terminalManager.activeTerminal?.webView.becomeFirstResponder()
}

var body: some View {
Expand Down Expand Up @@ -232,7 +232,7 @@ private struct MainView: View {
App.setUpEditorInstance()
}
.onChange(of: terminalOptions) { newValue in
App.terminalInstance.options = newValue.value
App.terminalManager.applyOptionsToAll(newValue.value)
}
.hiddenScrollableContentBackground()
.onAppear {
Expand Down
5 changes: 3 additions & 2 deletions CodeApp/Containers/RemoteContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ struct RemoteContainer: View {
App.loadRepository(url: hostUrl)
App.notificationManager.showInformationMessage(
"remote.connected")
App.terminalInstance.terminalServiceProvider =
App.workSpaceStorage.terminalServiceProvider
// Set terminal service provider for the active terminal
App.terminalManager.setTerminalServiceProviderForAll(
App.workSpaceStorage.terminalServiceProvider)
}
continuation.resume(returning: ())
}
Expand Down
20 changes: 18 additions & 2 deletions CodeApp/Managers/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Executor {
case interactive
}

private let persistentIdentifier = "com.thebaselab.terminal"
private let persistentIdentifier: String
private var pid: pid_t? = nil

private var stdin_file: UnsafeMutablePointer<FILE>?
Expand All @@ -40,10 +40,13 @@ class Executor {
}

init(
root: URL, onStdout: @escaping ((_ data: Data) -> Void),
root: URL,
sessionIdentifier: String = "com.thebaselab.terminal",
onStdout: @escaping ((_ data: Data) -> Void),
onStderr: @escaping ((_ data: Data) -> Void),
onRequestInput: @escaping ((_ prompt: String) -> Void)
) {
persistentIdentifier = sessionIdentifier
currentWorkingDirectory = root
prompt = "\(root.lastPathComponent) $ "
receivedStdout = onStdout
Expand Down Expand Up @@ -283,3 +286,16 @@ class Executor {
}
}
}

extension Executor.State {
var displayName: String {
switch self {
case .idle:
return NSLocalizedString("Idle", comment: "Executor state label")
case .running:
return NSLocalizedString("Running", comment: "Executor state label")
case .interactive:
return NSLocalizedString("Interactive", comment: "Executor state label")
}
}
}
71 changes: 59 additions & 12 deletions CodeApp/Managers/MainApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import SwiftGit2
import SwiftUI
import UniformTypeIdentifiers
import ios_system
import os.log

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Code", category: "MainApp")

struct CheckoutDestination: Identifiable {
var id = UUID()
Expand Down Expand Up @@ -191,8 +194,13 @@ class MainApp: ObservableObject {
var editorShortcuts: [MonacoEditorAction] = []
var monacoStateToRestore: String? = nil

var terminalInstance: TerminalInstance! = nil
let terminalManager: TerminalManager
var monacoInstance: EditorImplementation! = nil

// Backward compatibility: returns the active terminal
var terminalInstance: TerminalInstance! {
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backward compatibility property terminalInstance returns the active terminal, which could be nil (since activeTerminal is optional), but the property is declared as an implicitly unwrapped optional TerminalInstance!. This could lead to crashes if code expects a non-nil value but no active terminal exists. While the TerminalManager ensures at least one terminal exists after initialization, there might be edge cases during cleanup or state transitions where this assumption breaks. Consider either making this a regular optional TerminalInstance? to force callers to handle the nil case explicitly, or add documentation clarifying the invariant that this will never be nil after initialization.

Suggested change
var terminalInstance: TerminalInstance! {
var terminalInstance: TerminalInstance? {

Copilot uses AI. Check for mistakes.
terminalManager.activeTerminal
}
var editorTypesMonitor: FolderMonitor? = nil
let deviceSupportsBiometricAuth: Bool = biometricAuthSupported()
let sceneIdentifier = UUID()
Expand All @@ -202,6 +210,8 @@ class MainApp: ObservableObject {
private var searchCancellable: AnyCancellable? = nil
private var textSearchCancellable: AnyCancellable? = nil
private var workSpaceCancellable: AnyCancellable? = nil
private var cancellables = Set<AnyCancellable>()
private var isConfiguringOpenEditors = false

@AppStorage("alwaysOpenInNewTab") var alwaysOpenInNewTab: Bool = false
@AppStorage("explorer.confirmBeforeDelete") var confirmBeforeDelete = false
Expand All @@ -221,18 +231,23 @@ class MainApp: ObservableObject {

self.workSpaceStorage = WorkSpaceStorage(url: rootDir)

terminalInstance = TerminalInstance(root: rootDir, options: terminalOptions.value)
// Use helper to read options before self is fully initialized
let options = TerminalManager.readTerminalOptionsFromDefaults()
self.terminalManager = TerminalManager(rootURL: rootDir, options: options)
setUpEditorInstance()

terminalInstance.openEditor = { [weak self] url in
if url.isDirectory {
DispatchQueue.main.async {
self?.loadFolder(url: url)
}
} else {
self?.openFile(url: url)
// Set up openEditor callback for initial terminal
configureOpenEditorForTerminals()

// Forward terminalManager changes to MainApp so UI updates
terminalManager.objectWillChange.sink { [weak self] _ in
DispatchQueue.main.async {
guard let self = self else { return }
self.objectWillChange.send()
// Set up openEditor for any new terminals
self.scheduleConfigureOpenEditorForTerminals()
}
}
}.store(in: &cancellables)

// TODO: Support deleted files detection for remote files
workSpaceStorage.onDirectoryChange { [weak self] url in
Expand All @@ -248,7 +263,15 @@ class MainApp: ObservableObject {
}
}
workSpaceStorage.onTerminalData { [weak self] data in
self?.terminalInstance.write(data: data)
guard let self = self else { return }
// Use the tracked remote terminal for consistent data routing
if let terminal = self.terminalManager.remoteTerminal {
terminal.write(data: data)
} else {
logger.warning(
"Remote terminal data dropped: no remote terminal available (\(data.count) bytes)"
)
}
}
loadRepository(url: rootDir)

Expand Down Expand Up @@ -300,6 +323,30 @@ class MainApp: ObservableObject {
}
}

private func configureOpenEditorForTerminals() {
for terminal in terminalManager.terminals where terminal.openEditor == nil {
terminal.openEditor = { [weak self] url in
if url.isDirectory {
DispatchQueue.main.async {
self?.loadFolder(url: url)
}
} else {
self?.openFile(url: url)
Comment on lines 325 to +334
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The openEditor closure in the configureOpenEditorForTerminals method captures self weakly, which is good for preventing retain cycles. However, consider that the closure is assigned to terminal.openEditor and may outlive the MainApp instance in edge cases. While the weak reference should prevent crashes, verify that the terminal cleanup properly nils out the openEditor closure to avoid unnecessary references.

Copilot uses AI. Check for mistakes.
}
}
}
}

private func scheduleConfigureOpenEditorForTerminals() {
guard !isConfiguringOpenEditors else { return }
isConfiguringOpenEditors = true
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.configureOpenEditorForTerminals()
self.isConfiguringOpenEditors = false
}
}

private func updateActiveEditor() async {
guard let activeTextEditor else {
Task {
Expand Down Expand Up @@ -937,7 +984,7 @@ class MainApp: ObservableObject {
if resetEditors {
DispatchQueue.main.async {
self.closeAllEditors()
self.terminalInstance.resetAndSetNewRootDirectory(url: url)
self.terminalManager.resetAndSetNewRootDirectory(url: url)
}
}
extensionManager.onWorkSpaceStorageChanged(newUrl: url)
Expand Down
40 changes: 35 additions & 5 deletions CodeApp/Managers/TerminalInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ struct TerminalOptions: Codable {
// subsequent options must be made optional
}

class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate, Identifiable {

let id: UUID
var name: String
private(set) var isReady = false

var options: TerminalOptions {
didSet {
Expand All @@ -34,11 +38,10 @@ class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
if terminalServiceProvider != nil {
self.startInteractive()
}
terminalServiceProvider?.onDisconnect(callback: {
if terminalServiceProvider == nil && oldValue != nil {
self.stopInteractive()
self.terminalServiceProvider = nil
self.clearLine()
})
}
}
}
public var webView: WebViewBase = WebViewBase()
Expand Down Expand Up @@ -383,6 +386,8 @@ class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
self.applyTheme(rawTheme: darkTheme.dictionary)
}
configureCustomOptions()
isReady = true
NotificationCenter.default.post(name: .terminalDidInitialize, object: self)
case "window.size.change":
let cols = result["Cols"] as! Int
let rows = result["Rows"] as! Int
Expand Down Expand Up @@ -440,11 +445,14 @@ class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
}
}

init(root: URL, options: TerminalOptions) {
init(root: URL, options: TerminalOptions, name: String = "Terminal", id: UUID = UUID()) {
self.id = id
self.name = name
self.options = options
super.init()
self.executor = Executor(
root: root,
sessionIdentifier: "com.thebaselab.terminal.\(id.uuidString)",
onStdout: { [weak self] data in
self?.writeToLocalTerminal(data: data)
},
Expand Down Expand Up @@ -535,6 +543,27 @@ extension TerminalInstance: WKUIDelegate {
}
}

// Cleanup method

extension TerminalInstance {
/// Cleans up resources before the terminal is deallocated.
/// Call this method before removing the terminal from TerminalManager.
func cleanup() {
if executor?.state != .idle {
executor?.kill()
}
terminalServiceProvider?.kill()
terminalServiceProvider = nil
webView.stopLoading()
webView.navigationDelegate = nil
webView.uiDelegate = nil
webView.removeFromSuperview()
webView.configuration.userContentController.removeAllScriptMessageHandlers()
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup method calls removeAllScriptMessageHandlers(), but the initialization only adds a script message handler if !terminalMessageHandlerAdded is true. This flag is an instance variable that starts as false, so the first terminal instance will add a handler. However, removeAllScriptMessageHandlers() removes ALL handlers from the shared UserContentController, which could affect other terminal instances or components sharing the same configuration. This could lead to message handlers being unexpectedly removed from active terminals. Consider using removeScriptMessageHandler(forName:) to remove only the specific handler for this terminal instance.

Suggested change
webView.configuration.userContentController.removeAllScriptMessageHandlers()

Copilot uses AI. Check for mistakes.
executor = nil
openEditor = nil
}
}

// Keyboard toolbar methods

extension TerminalInstance {
Expand Down Expand Up @@ -567,4 +596,5 @@ extension TerminalInstance {
extension Notification.Name {
static let terminalControlReset = Notification.Name("terminalControlReset")
static let terminalAltReset = Notification.Name("terminalAltReset")
static let terminalDidInitialize = Notification.Name("terminalDidInitialize")
}
Loading