haltija 1.1.21 → 1.2.3
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/apps/desktop/index.html +1 -1
- package/apps/desktop/main.js +264 -64
- package/apps/desktop/package.json +11 -3
- package/apps/desktop/preload.js +17 -0
- package/apps/desktop/renderer/agent-status.js +210 -0
- package/apps/desktop/renderer/settings.js +55 -0
- package/apps/desktop/renderer/state.js +98 -0
- package/apps/desktop/renderer/status.js +38 -0
- package/apps/desktop/renderer/tabs.js +374 -0
- package/apps/desktop/renderer/ui-utils.js +180 -0
- package/apps/desktop/renderer/video-capture.js +154 -0
- package/apps/desktop/renderer/webview-events.js +225 -0
- package/apps/desktop/renderer.js +98 -1604
- package/apps/desktop/resources/component.js +265 -55
- package/apps/desktop/webview-preload.js +19 -1
- package/bin/cli-subcommand.mjs +90 -27
- package/bin/hints.json +9 -4
- package/bin/hj.mjs +61 -2
- package/bin/test-data.mjs +291 -0
- package/bin/tosijs-dev.mjs +95 -20
- package/dist/client.js +5 -1
- package/dist/component.js +265 -55
- package/dist/hj.js +1600 -0
- package/dist/index.js +473 -91
- package/dist/server.js +473 -91
- package/dist/test.js +847 -0
- package/package.json +6 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings modal and new tab dialog.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { settings, saveSettings as persistSettings, el } from './state.js'
|
|
6
|
+
import { checkHaltija } from './status.js'
|
|
7
|
+
|
|
8
|
+
let pendingNewTabResolve = null
|
|
9
|
+
|
|
10
|
+
export function showNewTabDialog(url) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
pendingNewTabResolve = resolve
|
|
13
|
+
el.newTabUrlEl.textContent = url
|
|
14
|
+
el.newTabDialog.classList.remove('hidden')
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hideNewTabDialog(allowed) {
|
|
19
|
+
el.newTabDialog.classList.add('hidden')
|
|
20
|
+
if (pendingNewTabResolve) {
|
|
21
|
+
pendingNewTabResolve(allowed)
|
|
22
|
+
pendingNewTabResolve = null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function showSettings() {
|
|
27
|
+
document.querySelector(`input[name="server-mode"][value="${settings.serverMode}"]`).checked = true
|
|
28
|
+
document.getElementById('server-url').value = settings.serverUrl
|
|
29
|
+
document.getElementById('confirm-new-tabs').checked = settings.confirmNewTabs
|
|
30
|
+
el.settingsModal.classList.remove('hidden')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hideSettings() {
|
|
34
|
+
el.settingsModal.classList.add('hidden')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function applySettings() {
|
|
38
|
+
settings.serverMode = document.querySelector('input[name="server-mode"]:checked').value
|
|
39
|
+
settings.serverUrl = document.getElementById('server-url').value || 'http://localhost:8700'
|
|
40
|
+
settings.confirmNewTabs = document.getElementById('confirm-new-tabs').checked
|
|
41
|
+
persistSettings()
|
|
42
|
+
hideSettings()
|
|
43
|
+
checkHaltija()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function initSettingsListeners() {
|
|
47
|
+
el.settingsBtn.addEventListener('click', showSettings)
|
|
48
|
+
el.closeSettingsBtn.addEventListener('click', hideSettings)
|
|
49
|
+
el.saveSettingsBtn.addEventListener('click', applySettings)
|
|
50
|
+
el.settingsModal.addEventListener('click', (e) => {
|
|
51
|
+
if (e.target === el.settingsModal) hideSettings()
|
|
52
|
+
})
|
|
53
|
+
el.allowNewTabBtn.addEventListener('click', () => hideNewTabDialog(true))
|
|
54
|
+
el.denyNewTabBtn.addEventListener('click', () => hideNewTabDialog(false))
|
|
55
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared renderer state — tab list, settings, DOM element references.
|
|
3
|
+
* All modules import from here instead of accessing globals.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Settings
|
|
7
|
+
const DEFAULT_SETTINGS = {
|
|
8
|
+
serverMode: 'builtin',
|
|
9
|
+
serverUrl: 'http://localhost:8700',
|
|
10
|
+
confirmNewTabs: false,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export let settings = { ...DEFAULT_SETTINGS }
|
|
14
|
+
|
|
15
|
+
export function loadSettings() {
|
|
16
|
+
try {
|
|
17
|
+
const saved = localStorage.getItem('haltija-settings')
|
|
18
|
+
if (saved) {
|
|
19
|
+
settings = { ...DEFAULT_SETTINGS, ...JSON.parse(saved) }
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('[Haltija Desktop] Failed to load settings:', e)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveSettings() {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem('haltija-settings', JSON.stringify(settings))
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error('[Haltija Desktop] Failed to save settings:', e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getServerUrl() {
|
|
35
|
+
return settings.serverUrl || DEFAULT_SETTINGS.serverUrl
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Tab state
|
|
39
|
+
export const tabs = []
|
|
40
|
+
export let activeTabId = null
|
|
41
|
+
export let tabIdCounter = 0
|
|
42
|
+
export let lastCwd = localStorage.getItem('haltija-lastCwd') || null
|
|
43
|
+
|
|
44
|
+
export function setActiveTabId(id) { activeTabId = id }
|
|
45
|
+
export function nextTabId() { return `tab-${++tabIdCounter}` }
|
|
46
|
+
export function setLastCwd(cwd) {
|
|
47
|
+
lastCwd = cwd
|
|
48
|
+
localStorage.setItem('haltija-lastCwd', cwd)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// DOM element references (initialized in renderer.js after DOM is ready)
|
|
52
|
+
export const el = {
|
|
53
|
+
tabBar: null,
|
|
54
|
+
newTabButton: null,
|
|
55
|
+
toolbar: null,
|
|
56
|
+
urlInput: null,
|
|
57
|
+
goButton: null,
|
|
58
|
+
backButton: null,
|
|
59
|
+
forwardButton: null,
|
|
60
|
+
refreshButton: null,
|
|
61
|
+
webviewContainer: null,
|
|
62
|
+
statusDot: null,
|
|
63
|
+
settingsBtn: null,
|
|
64
|
+
settingsModal: null,
|
|
65
|
+
closeSettingsBtn: null,
|
|
66
|
+
saveSettingsBtn: null,
|
|
67
|
+
newTabDialog: null,
|
|
68
|
+
newTabUrlEl: null,
|
|
69
|
+
allowNewTabBtn: null,
|
|
70
|
+
denyNewTabBtn: null,
|
|
71
|
+
agentStatusBar: null,
|
|
72
|
+
agentStatusItems: null,
|
|
73
|
+
agentSelect: null,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function initElements() {
|
|
77
|
+
el.tabBar = document.getElementById('tabs')
|
|
78
|
+
el.newTabButton = document.getElementById('new-tab')
|
|
79
|
+
el.toolbar = document.getElementById('toolbar')
|
|
80
|
+
el.urlInput = document.getElementById('url-input')
|
|
81
|
+
el.goButton = document.getElementById('go')
|
|
82
|
+
el.backButton = document.getElementById('back')
|
|
83
|
+
el.forwardButton = document.getElementById('forward')
|
|
84
|
+
el.refreshButton = document.getElementById('refresh')
|
|
85
|
+
el.webviewContainer = document.getElementById('webview-container')
|
|
86
|
+
el.statusDot = document.getElementById('haltija-status')
|
|
87
|
+
el.settingsBtn = document.getElementById('settings-btn')
|
|
88
|
+
el.settingsModal = document.getElementById('settings-modal')
|
|
89
|
+
el.closeSettingsBtn = document.getElementById('close-settings')
|
|
90
|
+
el.saveSettingsBtn = document.getElementById('save-settings')
|
|
91
|
+
el.newTabDialog = document.getElementById('new-tab-dialog')
|
|
92
|
+
el.newTabUrlEl = document.getElementById('new-tab-url')
|
|
93
|
+
el.allowNewTabBtn = document.getElementById('allow-new-tab')
|
|
94
|
+
el.denyNewTabBtn = document.getElementById('deny-new-tab')
|
|
95
|
+
el.agentStatusBar = document.getElementById('agent-status-bar')
|
|
96
|
+
el.agentStatusItems = document.getElementById('agent-status-items')
|
|
97
|
+
el.agentSelect = document.getElementById('agent-select')
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server status checking and nav button updates.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { el, getServerUrl } from './state.js'
|
|
6
|
+
|
|
7
|
+
export async function checkHaltija() {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(`${getServerUrl()}/status`)
|
|
10
|
+
if (response.ok) {
|
|
11
|
+
el.statusDot.className = 'status-dot connected'
|
|
12
|
+
el.statusDot.title = 'Haltija: Connected'
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error('Not OK')
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
el.statusDot.className = 'status-dot disconnected'
|
|
18
|
+
el.statusDot.title = 'Haltija: Disconnected - Start server with: bunx haltija'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function updateNavButtons() {
|
|
23
|
+
// Import dynamically to avoid circular dependency
|
|
24
|
+
const { getActiveWebview } = window._tabs
|
|
25
|
+
const webview = getActiveWebview()
|
|
26
|
+
if (webview) {
|
|
27
|
+
try {
|
|
28
|
+
el.backButton.disabled = !webview.canGoBack()
|
|
29
|
+
el.forwardButton.disabled = !webview.canGoForward()
|
|
30
|
+
} catch (e) {
|
|
31
|
+
el.backButton.disabled = true
|
|
32
|
+
el.forwardButton.disabled = true
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
el.backButton.disabled = true
|
|
36
|
+
el.forwardButton.disabled = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab management — create, activate, close, navigate browser tabs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tabs, activeTabId, setActiveTabId, nextTabId, el, getServerUrl, lastCwd, setLastCwd } from './state.js'
|
|
6
|
+
import { showNotification } from './ui-utils.js'
|
|
7
|
+
import { setupWebviewEvents } from './webview-events.js'
|
|
8
|
+
import { checkHaltija, updateNavButtons } from './status.js'
|
|
9
|
+
|
|
10
|
+
export function getDefaultUrl() {
|
|
11
|
+
return `${getServerUrl()}/test`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createTab(url, activate = true) {
|
|
15
|
+
const tabId = nextTabId()
|
|
16
|
+
const tabUrl = url || getDefaultUrl()
|
|
17
|
+
|
|
18
|
+
const tabEl = document.createElement('div')
|
|
19
|
+
tabEl.className = 'tab'
|
|
20
|
+
tabEl.dataset.tabId = tabId
|
|
21
|
+
tabEl.innerHTML = `
|
|
22
|
+
<span class="tab-title">New Tab</span>
|
|
23
|
+
<button class="tab-close" title="Close tab">×</button>
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
tabEl.addEventListener('click', (e) => {
|
|
27
|
+
if (!e.target.classList.contains('tab-close')) {
|
|
28
|
+
activateTab(tabId)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
tabEl.querySelector('.tab-close').addEventListener('click', (e) => {
|
|
33
|
+
e.stopPropagation()
|
|
34
|
+
closeTab(tabId)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
el.tabBar.appendChild(tabEl)
|
|
38
|
+
|
|
39
|
+
const webview = document.createElement('webview')
|
|
40
|
+
webview.id = tabId
|
|
41
|
+
webview.src = 'about:blank'
|
|
42
|
+
if (window.haltija?.webviewPreloadPath) {
|
|
43
|
+
webview.setAttribute('preload', 'file://' + window.haltija.webviewPreloadPath)
|
|
44
|
+
}
|
|
45
|
+
webview.setAttribute(
|
|
46
|
+
'webpreferences',
|
|
47
|
+
'contextIsolation=yes, nodeIntegration=no, webSecurity=no, allowRunningInsecureContent=yes',
|
|
48
|
+
)
|
|
49
|
+
webview.setAttribute('allowpopups', '')
|
|
50
|
+
|
|
51
|
+
el.webviewContainer.appendChild(webview)
|
|
52
|
+
|
|
53
|
+
const tab = {
|
|
54
|
+
id: tabId,
|
|
55
|
+
url: tabUrl,
|
|
56
|
+
title: 'New Tab',
|
|
57
|
+
element: tabEl,
|
|
58
|
+
webview: webview,
|
|
59
|
+
}
|
|
60
|
+
tabs.push(tab)
|
|
61
|
+
|
|
62
|
+
setupWebviewEvents(tab)
|
|
63
|
+
|
|
64
|
+
if (activate) {
|
|
65
|
+
activateTab(tabId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
webview.addEventListener('did-attach', () => {
|
|
69
|
+
navigate(tabUrl, tabId)
|
|
70
|
+
}, { once: true })
|
|
71
|
+
|
|
72
|
+
const fallbackTabId = tabId
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
const t = tabs.find(tt => tt.id === fallbackTabId)
|
|
75
|
+
if (t && !t.isTerminal && (webview.getURL() === 'about:blank' || !webview.getURL())) {
|
|
76
|
+
navigate(tabUrl, fallbackTabId)
|
|
77
|
+
}
|
|
78
|
+
}, 500)
|
|
79
|
+
|
|
80
|
+
return tab
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function createTerminalTab(mode = 'human') {
|
|
84
|
+
const tabId = nextTabId()
|
|
85
|
+
const isAgent = mode === 'agent'
|
|
86
|
+
const prefix = isAgent ? '<span class="agent-status">*</span>' : '>'
|
|
87
|
+
|
|
88
|
+
if (isAgent) {
|
|
89
|
+
await checkHjInstalled()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const activeTab = getActiveTab()
|
|
93
|
+
const initialCwd = (activeTab?.isTerminal && activeTab?.cwd) || lastCwd || ''
|
|
94
|
+
const label = isAgent ? 'agent' : 'shell'
|
|
95
|
+
|
|
96
|
+
const tabEl = document.createElement('div')
|
|
97
|
+
tabEl.className = `tab ${isAgent ? 'agent ready' : 'terminal'}`
|
|
98
|
+
tabEl.dataset.tabId = tabId
|
|
99
|
+
tabEl.innerHTML = `
|
|
100
|
+
<span class="tab-title">${prefix} ${label}</span>
|
|
101
|
+
<button class="tab-close" title="Close tab">×</button>
|
|
102
|
+
`
|
|
103
|
+
|
|
104
|
+
tabEl.addEventListener('click', (e) => {
|
|
105
|
+
if (!e.target.classList.contains('tab-close')) {
|
|
106
|
+
activateTab(tabId)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
tabEl.querySelector('.tab-close').addEventListener('click', (e) => {
|
|
111
|
+
e.stopPropagation()
|
|
112
|
+
closeTab(tabId)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
el.tabBar.appendChild(tabEl)
|
|
116
|
+
|
|
117
|
+
const iframe = document.createElement('iframe')
|
|
118
|
+
iframe.id = tabId
|
|
119
|
+
const cwdParam = initialCwd ? `&cwd=${encodeURIComponent(initialCwd)}` : ''
|
|
120
|
+
iframe.src = `terminal.html?port=${window.haltija?.port || 8700}&mode=${mode}${cwdParam}`
|
|
121
|
+
iframe.className = 'terminal-frame'
|
|
122
|
+
|
|
123
|
+
el.webviewContainer.appendChild(iframe)
|
|
124
|
+
|
|
125
|
+
const tab = {
|
|
126
|
+
id: tabId,
|
|
127
|
+
url: 'terminal',
|
|
128
|
+
title: label,
|
|
129
|
+
element: tabEl,
|
|
130
|
+
webview: iframe,
|
|
131
|
+
isTerminal: true,
|
|
132
|
+
terminalMode: mode,
|
|
133
|
+
}
|
|
134
|
+
tabs.push(tab)
|
|
135
|
+
|
|
136
|
+
activateTab(tabId)
|
|
137
|
+
return tab
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function renameTerminalTab(tab, name) {
|
|
141
|
+
if (!name) return
|
|
142
|
+
tab.shellName = name
|
|
143
|
+
tab.title = name
|
|
144
|
+
const prefix = tab.terminalMode === 'agent' ? '*' : '>'
|
|
145
|
+
tab.element.querySelector('.tab-title').textContent = `${prefix} ${name}`
|
|
146
|
+
document.title = name
|
|
147
|
+
const iframe = tab.webview
|
|
148
|
+
if (iframe && iframe.contentWindow) {
|
|
149
|
+
iframe.contentWindow.postMessage({ type: 'rename', name }, '*')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function activateTab(tabId) {
|
|
154
|
+
const tab = tabs.find((t) => t.id === tabId)
|
|
155
|
+
if (!tab) return
|
|
156
|
+
|
|
157
|
+
tabs.forEach((t) => {
|
|
158
|
+
t.element.classList.remove('active')
|
|
159
|
+
t.webview.classList.remove('active')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
tab.element.classList.add('active')
|
|
163
|
+
tab.webview.classList.add('active')
|
|
164
|
+
setActiveTabId(tabId)
|
|
165
|
+
|
|
166
|
+
el.toolbar.classList.remove('terminal', 'agent')
|
|
167
|
+
if (tab.isTerminal || tab.url === 'terminal') {
|
|
168
|
+
el.urlInput.value = tab.cwd || '~'
|
|
169
|
+
el.urlInput.placeholder = 'working directory'
|
|
170
|
+
el.goButton.textContent = 'Pick folder\u2026'
|
|
171
|
+
el.goButton.title = 'Pick folder\u2026'
|
|
172
|
+
el.toolbar.classList.add(tab.terminalMode === 'agent' ? 'agent' : 'terminal')
|
|
173
|
+
|
|
174
|
+
if (window._currentStatusLine) {
|
|
175
|
+
el.agentStatusBar.classList.remove('hidden')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (tab.terminalMode === 'agent' && tab.shellId) {
|
|
179
|
+
fetch(`${getServerUrl()}/terminal/agent-focus`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ shellId: tab.shellId }),
|
|
183
|
+
}).catch(() => {})
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
el.urlInput.value = tab.url || ''
|
|
187
|
+
el.urlInput.placeholder = 'Enter URL...'
|
|
188
|
+
el.goButton.textContent = 'Go'
|
|
189
|
+
el.goButton.title = 'Go'
|
|
190
|
+
|
|
191
|
+
el.agentStatusBar.classList.add('hidden')
|
|
192
|
+
}
|
|
193
|
+
document.title = tab.title || 'Haltija'
|
|
194
|
+
|
|
195
|
+
updateNavButtons()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function findTabByWindowId(windowId) {
|
|
199
|
+
if (!windowId) return null
|
|
200
|
+
const wcId = parseInt(windowId.split('-').pop(), 10)
|
|
201
|
+
if (isNaN(wcId)) return null
|
|
202
|
+
return tabs.find(t => {
|
|
203
|
+
try {
|
|
204
|
+
return t.webview && t.webview.getWebContentsId && t.webview.getWebContentsId() === wcId
|
|
205
|
+
} catch { return false }
|
|
206
|
+
}) || null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function closeTab(tabId) {
|
|
210
|
+
const tabIndex = tabs.findIndex((t) => t.id === tabId)
|
|
211
|
+
if (tabIndex === -1) return
|
|
212
|
+
|
|
213
|
+
const tab = tabs[tabIndex]
|
|
214
|
+
|
|
215
|
+
if (tab.webview && tab.webview.tagName === 'WEBVIEW') {
|
|
216
|
+
try {
|
|
217
|
+
tab.webview.stop()
|
|
218
|
+
tab.webview.src = 'about:blank'
|
|
219
|
+
} catch (e) {}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
tab.element.remove()
|
|
223
|
+
tab.webview.remove()
|
|
224
|
+
|
|
225
|
+
tabs.splice(tabIndex, 1)
|
|
226
|
+
|
|
227
|
+
if (activeTabId === tabId) {
|
|
228
|
+
if (tabs.length > 0) {
|
|
229
|
+
const newIndex = Math.max(0, tabIndex - 1)
|
|
230
|
+
activateTab(tabs[newIndex].id)
|
|
231
|
+
} else {
|
|
232
|
+
window.haltija.closeWindow()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getActiveTab() {
|
|
238
|
+
return tabs.find((t) => t.id === activeTabId)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getActiveWebview() {
|
|
242
|
+
const tab = getActiveTab()
|
|
243
|
+
return tab ? tab.webview : null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function navigate(url, tabId = activeTabId) {
|
|
247
|
+
const tab = tabs.find((t) => t.id === tabId)
|
|
248
|
+
if (!tab) return
|
|
249
|
+
|
|
250
|
+
let addedHttps = false
|
|
251
|
+
|
|
252
|
+
if (url && !url.match(/^(https?|blob|data|file|about|javascript):\/?\/?/i)) {
|
|
253
|
+
if (url.includes('.') || url === 'localhost' || url.startsWith('localhost:')) {
|
|
254
|
+
addedHttps = true
|
|
255
|
+
url = 'https://' + url
|
|
256
|
+
} else {
|
|
257
|
+
url = 'https://www.google.com/search?q=' + encodeURIComponent(url)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
tab.url = url || getDefaultUrl()
|
|
262
|
+
|
|
263
|
+
if (addedHttps) {
|
|
264
|
+
const httpUrl = tab.url.replace(/^https:/, 'http:')
|
|
265
|
+
|
|
266
|
+
const failHandler = (e) => {
|
|
267
|
+
if (e.errorCode < 0) {
|
|
268
|
+
console.log(`[Haltija] HTTPS failed (${e.errorCode}), trying HTTP...`)
|
|
269
|
+
tab.webview.removeEventListener('did-fail-load', failHandler)
|
|
270
|
+
tab.url = httpUrl
|
|
271
|
+
tab.webview.src = httpUrl
|
|
272
|
+
if (tabId === activeTabId) {
|
|
273
|
+
el.urlInput.value = httpUrl
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
tab.webview.addEventListener('did-fail-load', failHandler, { once: true })
|
|
278
|
+
|
|
279
|
+
tab.webview.addEventListener('did-finish-load', () => {
|
|
280
|
+
tab.webview.removeEventListener('did-fail-load', failHandler)
|
|
281
|
+
}, { once: true })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
tab.webview.src = tab.url
|
|
285
|
+
|
|
286
|
+
if (tabId === activeTabId && !tab.isTerminal) {
|
|
287
|
+
el.urlInput.value = tab.url
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function changeTerminalDirectory(tab, path) {
|
|
292
|
+
if (!path) return
|
|
293
|
+
try {
|
|
294
|
+
const resp = await fetch(`${getServerUrl()}/terminal/command`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ command: `cd ${path}`, shellId: tab.shellId }),
|
|
298
|
+
})
|
|
299
|
+
const result = await resp.text()
|
|
300
|
+
if (result.startsWith('cd:')) {
|
|
301
|
+
showNotification(result, 3000)
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
showNotification(`Error: ${err.message}`, 3000)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function openFolderPicker(tab) {
|
|
309
|
+
if (window.haltija?.showOpenDialog) {
|
|
310
|
+
const result = await window.haltija.showOpenDialog({
|
|
311
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
312
|
+
defaultPath: tab.cwd || undefined,
|
|
313
|
+
})
|
|
314
|
+
if (result && !result.canceled && result.filePaths?.[0]) {
|
|
315
|
+
const picked = result.filePaths[0]
|
|
316
|
+
setLastCwd(picked)
|
|
317
|
+
changeTerminalDirectory(tab, picked)
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
showNotification('Folder picker not available', 2000)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// hj CLI install check
|
|
325
|
+
let hjCheckDone = false
|
|
326
|
+
|
|
327
|
+
async function checkHjInstalled() {
|
|
328
|
+
if (hjCheckDone) return
|
|
329
|
+
hjCheckDone = true
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const response = await fetch(`${getServerUrl()}/terminal/hj-status`)
|
|
333
|
+
if (!response.ok) return
|
|
334
|
+
|
|
335
|
+
const { installed, installCommand, message } = await response.json()
|
|
336
|
+
if (installed) return
|
|
337
|
+
|
|
338
|
+
showHjInstallPrompt(installCommand, message)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.log('[Haltija Desktop] Could not check hj status:', err.message)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function showHjInstallPrompt(installCommand, message) {
|
|
345
|
+
const prompt = document.createElement('div')
|
|
346
|
+
prompt.className = 'hj-install-prompt'
|
|
347
|
+
prompt.innerHTML = `
|
|
348
|
+
<div class="hj-install-content">
|
|
349
|
+
<div class="hj-install-header">
|
|
350
|
+
<span class="hj-install-icon">\u26A0\uFE0F</span>
|
|
351
|
+
<span class="hj-install-title">hj CLI not installed</span>
|
|
352
|
+
</div>
|
|
353
|
+
<p class="hj-install-message">${message}</p>
|
|
354
|
+
<div class="hj-install-command">
|
|
355
|
+
<code>${installCommand}</code>
|
|
356
|
+
<button class="hj-copy-btn" title="Copy command">Copy</button>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="hj-install-actions">
|
|
359
|
+
<button class="hj-install-later">Later</button>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
`
|
|
363
|
+
|
|
364
|
+
prompt.querySelector('.hj-copy-btn').addEventListener('click', () => {
|
|
365
|
+
navigator.clipboard.writeText(installCommand)
|
|
366
|
+
showNotification('Command copied! Paste in Terminal and enter your password.', 5000)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
prompt.querySelector('.hj-install-later').addEventListener('click', () => {
|
|
370
|
+
prompt.remove()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
document.body.appendChild(prompt)
|
|
374
|
+
}
|